前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
  • Vue

    • 从源码解读Vue生命周期
    • 组件的本质
    • 有状态组件的设计
    • 设计 VNode
    • 辅助创建 VNode 的 h 函数
    • 自定义渲染器和异步渲染
    • 渲染器之挂载
    • 渲染器的核心 Diff 算法
    • 渲染器之patch
    • 图解 Vue 响应式原理
    • 图解 Vue 异步更新
    • 剖析 Vue 内部运行机制
    • vue响应式原理模拟
    • vue状态管理之vuex
    • 理解Vue的设计思想及实现Vue
    • diff算法深入
    • vue router vuex原理分析
    • Vue3初探响应式原理.
    • vue2源码分析
    • vue组件化实践
  • React

  • Webpack

  • Node

  • Javascript

  • 综合

完整面试题地址:
作者:程序员poetry
扫码关注作者公众号:「前端进阶之旅」 每天分享技术干货
前端进阶之旅公众号二维码

对比 Koa 和 Redux-分析前端中的中间件理念|原理篇

文章学习导航
面试总览

30 秒速记

  • 核心判断:状态管理的核心是把状态变化约束成可追踪的数据流,中间件在派发边界扩展异步、日志和副作用
  • 原理主线:围绕 「以 Koa 为代表的 Node.js 中」、「对比 Express,再谈 Koa 中间」、「Redux 中间件设计和实现」 建立输入、状态变化与输出之间的因果关系
  • 文章范围:分析了 Koa 和 Redux 两大前端中间件体系的设计理念与实现原理,详细对比了它们在中间件机制、应用场景和开发模式上的异同,帮助开发者理解中间件在前端架构中的核心作用和最佳实践。
  • 边界与代价:集中式状态不是越多越好;可变性、订阅粒度和异步取消决定性能与可维护性
  • 工程落地:选型要从状态归属、更新频率、调试需求和团队约束出发,而不是只比较 API 数量

Koa 和 Redux 的中间件都通过函数组合扩展核心流程,但一个围绕请求响应,另一个围绕 dispatch。 Koa 用 await next() 形成洋葱模型,下游结束后还能回到上游,很适合统一处理日志和异常。Redux 中间件把原始 dispatch 逐层包装,调用 next(action) 才会继续传递,不调用就会截断链路。异步调用 next 时,执行顺序会与同步情况不同,因此副作用和返回值需要格外留意。

这篇文章不要按 API 清单来背。先用上面的 Mind Map 建立全局结构,再通过交互 DEMO 观察正常路径和边界路径如何改变状态;阅读正文时重点核对每一步的输入、负责执行的参与者、产生的中间状态以及最终可观察结果。遇到版本敏感结论,要把“历史实现”“当前行为”和“工程兼容策略”分开说明;遇到性能或架构取舍,则用实际指标、失败现象和验证手段支撑判断。

版本校准: 本文若分析 ReactDOM.render、旧生命周期或栈调和,应把它视为理解架构演进的历史路径。React 19 已移除 ReactDOM.render,当前客户端入口使用 createRoot;并发渲染也必须区分可中断的渲染阶段与同步提交阶段。迁移前对照 React 19 官方升级指南。

# 以 Koa 为代表的 Node.js 中间件化设计

说到中间件,很多开发者都会想到 Koa.js,其中间件设计无疑是前端中间件思想的典型代表之一。我们先来剖析 Koa.js 的设计和实现。

先来看一下 Koa.js 中间件的实现和应用:

// 最外层中间件,可以用于兜底 Koa 全局错误
app.use(async (ctx, next) => {
  try {
    // console.log('中间件 1 开始执行')
    // 执行下一个中间件
    await next();
    // console.log('中间件 1 执行结束')
  } catch (error) {
    console.log(`[koa error]: ${error.message}`)
  }
});
// 第二层中间件,可以用于日志记录
app.use(async (ctx, next) => {
  // console.log('中间件 2 开始执行')
  const { req } = ctx;
  console.log(`req is ${JSON.stringify(req)}`);
  await next();
  console.log(`res is ${JSON.stringify(ctx.res)}`);
  // console.log('中间件 2 执行结束')
});
@前端进阶之旅: 代码已经复制到剪贴板

如上代码,我们看 Koa 实例,通过use方法注册和串联中间件,其源码实现部分精简表述为:

use(fn) {
    this.middleware.push(fn);
    return this;
}
@前端进阶之旅: 代码已经复制到剪贴板

如上代码,我们的中间件被存储进this.middleware数组中,那么中间件是如何被执行的呢?参考下面源码:

// 通过 createServer 方法启动一个 Node.js 服务
listen(...args) {
    const server = http.createServer(this.callback());
    return server.listen(...args);
}
@前端进阶之旅: 代码已经复制到剪贴板

Koa 框架通过 http 模块的 createServer 方法创建一个 Node.js 服务,并传入 this.callback() 方法, this.callback() 方法源码精简实现如下:

callback() {
    // 从 this.middleware 数组中,组合中间件
    const fn = compose(this.middleware);

    // handleRequest 方法作为 `http` 模块的 `createServer` 方法参数,该方法通过 `createContext` 封装了 `http.createServer` 中的 `request` 和 `response`对象,并将这两个对象放到 ctx 中
    const handleRequest = (req, res) => {
        const ctx = this.createContext(req, res);
        // 将 ctx 和组合后的中间件函数 fn 传递给 this.handleRequest 方法
        return this.handleRequest(ctx, fn);
    };

    return handleRequest;
}
handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;
    const onerror = err => ctx.onerror(err);
    const handleResponse = () => respond(ctx);
    // on-finished npm 包提供的方法,该方法在一个 HTTP 请求 closes,finishes 或者 errors 时执行
    onFinished(res, onerror);
    // 将 ctx 对象传递给中间件函数 fnMiddleware
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
@前端进阶之旅: 代码已经复制到剪贴板

如上代码,我们将 Koa 一个中间件组合和执行流程梳理为以下步骤。

  • 通过compose方法组合各种中间件,返回一个中间件组合函数fnMiddleware
  • 请求过来时,会先调用handleRequest方法,该方法完成
    • 调用createContext方法,对该次请求封装出一个ctx对象;
    • 接着调用this.handleRequest(ctx, fnMiddleware)处理该次请求。
  • 通过 fnMiddleware(ctx).then(handleResponse).catch(onerror) 执行中间件

其中,一个核心过程就是使用compose方法组合各种中间件,其源码实现精简为:

function compose(middleware) {
    // 这里返回的函数,就是上文中的 fnMiddleware
    return function (context, next) {
        let index = -1
        return dispatch(0)

        function dispatch(i) {
        	  // 
            if (i <= index) return Promise.reject(new Error('next() called multiple times'))
            index = i
            // 取出第 i 个中间件为 fn
            let fn = middleware[i]

            if (i === middleware.length) fn = next

            // 已经取到了最后一个中间件,直接返回一个 Promise 实例,进行串联
            // 这一步的意义是保证最后一个中间件调用 next 方法时,也不会报错
            if (!fn) return Promise.resolve()

            try {
                // 把 ctx 和 next 方法传入到中间件 fn 中,并将执行结果使用 Promise.resolve 包装
                // 这里可以发现,我们在一个中间件中调用的 next 方法,其实就是dispatch.bind(null, i + 1),即调用下一个中间件
                return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
            } catch (err) {
                return Promise.reject(err)
            }
        }
    }
}
@前端进阶之旅: 代码已经复制到剪贴板

源码实现中已加入了相关注释,如果对于你来说还是晦涩难懂,不妨看一下下面这个 hard coding 的例子,通过下面代码,表示三个 Koa 中间件的执行情况:

async function middleware1() {
  ...
  await (async function middleware2() {
    ...
    await (async function middleware3() {
      ...
    });
    ...
  });
  ...
}
@前端进阶之旅: 代码已经复制到剪贴板

这里我们来做一个简单的总结:

  • Koa 的中间件机制被社区形象地总结为洋葱模型;
    • 所谓洋葱模型,就是指每一个 Koa 中间件都是一层洋葱圈,它即可以掌管请求进入,也可以掌管响应返回。换句话说:外层的中间件可以影响内层的请求和响应阶段,内层的中间件只能影响外层的响应阶段。
  • dispatch(n)对应第 n 个中间件的执行,第 n 个中间件可以通过 await next() 来执行下一个中间件,同时在最后一个中间件执行完成后,依然有恢复执行的能力。即,通过洋葱模型,await next() 控制调用 “下游”中间件,直到 “下游”没有中间件且堆栈执行完毕,最终流回“上游”中间件。这种方式有个优点,特别是对于日志记录以及错误处理等需要非常友好。

这里我们稍微做一下扩展,引申出 Koa v1 版本中中间件的实现,Koa1 的中间件实现利用了 Generator 函数 + co 库(一种基于 Promise 的 Generator 函数流程管理工具),来实现协程运行。本质上,Koa v1 版本中间件和 Koa v2 版本中间件思想是类似的,只不过 Koa v2 主要是用了 Async/Await 来替换 Generator 函数 + co 库,整体实现更加巧妙,代码更加优雅、简易。

# 对比 Express,再谈 Koa 中间件

说起 Node.js 框架,我们自然忘不了 Express.js。它的中间件机制同样值得我们学习、比对。Express 不同于 Koa,它继承了路由、静态服务器和模板引擎等功能,因此看上去比 Koa 更像是一个框架。通过学习 Express 源码( https://github.com/expressjs/express),我们可以总结出它的工作机制。

← 小程序开发实践正则完整篇 →

fe
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
  • Vue

    • 从源码解读Vue生命周期
    • 组件的本质
    • 有状态组件的设计
    • 设计 VNode
    • 辅助创建 VNode 的 h 函数
    • 自定义渲染器和异步渲染
    • 渲染器之挂载
    • 渲染器的核心 Diff 算法
    • 渲染器之patch
    • 图解 Vue 响应式原理
    • 图解 Vue 异步更新
    • 剖析 Vue 内部运行机制
    • vue响应式原理模拟
    • vue状态管理之vuex
    • 理解Vue的设计思想及实现Vue
    • diff算法深入
    • vue router vuex原理分析
    • Vue3初探响应式原理.
    • vue2源码分析
    • vue组件化实践
  • React

  • Webpack

  • Node

  • Javascript

  • 综合